Skip to content

Conversation

@dsherret
Copy link
Member

@dsherret dsherret commented Nov 4, 2025

Adds the ability to ignore certain environment variables and return undefined instead of erroring.

> cat main.ts
console.log(Deno.env.get("VAR1"));
console.log(Deno.env.get("VAR2"));
console.log(Deno.env.toObject());
> VAR1=1 VAR2=2 deno run --ignore-env main.ts
undefined
undefined
[Object: null prototype] {}
> VAR1=1 VAR2=2 deno run --ignore-env=VAR1 --allow-env=VAR2 main.ts
undefined
2
[Object: null prototype] { VAR2: "2" }

Also doesn't error anymore when doing --allow-env --deny-env=SOMETHING and getting all env vars.

Closes #30891
Closes #31071

@bartlomieju
Copy link
Member

Closes #31071?

@dsherret dsherret marked this pull request as ready for review November 5, 2025 16:14
@bartlomieju
Copy link
Member

Can you please update the docs for permissions and show an example what happens if you use --ignore-env --deny-env=VAR1?

Comment on lines +179 to +182
fn check_env_with_maybe_exit(
state: &mut OpState,
key: &str,
) -> Result<ControlFlow<()>, PermissionCheckError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a nice solution 👍

@bartlomieju
Copy link
Member

Not a blocker, but we should update the interactive prompt with the i option as well - but it should be selective only to the env permission

Copilot AI review requested due to automatic review settings November 6, 2025 16:48
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for ignoring specific environment variables through the new --ignore-env flag and ignore configuration option. When an environment variable is ignored, attempts to access it return undefined (or are excluded from Deno.env.toObject()) without raising a permission error, providing a way to hide sensitive environment variables from scripts without requiring explicit permission denials.

Key Changes:

  • Introduces new PermissionState::Ignored and PermissionState::DeniedPartial states to the permission system
  • Adds --ignore-env CLI flag and corresponding configuration support for environment permissions
  • Refactors NODE_ENV_VAR_ALLOWLIST from a lazy-initialized HashSet to a sorted const array for more efficient lookups

Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/specs/run/permission_env_allow_and_deny/main.ts Updates test to verify env vars are properly filtered with allow/deny flags
tests/specs/run/permission_env_allow_and_deny/main.out Removes expected error output as test now succeeds with proper permission setup
tests/specs/run/permission_env_allow_and_deny/__test__.jsonc Updates test config to use allow/deny flags without expecting errors
tests/specs/permission/ignore_env/flags/main.ts New test for --ignore-env flag functionality
tests/specs/permission/ignore_env/flags/__test__.jsonc Test configurations for various --ignore-env scenarios (all, some, with allow-env, with deny-env)
tests/specs/permission/ignore_env/config/main.ts New test for ignore configuration via deno.json
tests/specs/permission/ignore_env/config/deno.json Config file demonstrating ignore: true setting for env permissions
tests/specs/permission/ignore_env/config/__test__.jsonc Test configuration for ignore via config file
runtime/permissions/lib.rs Core implementation: adds Ignored/DeniedPartial states, implements ignore list tracking, updates permission query logic
runtime/ops/permissions.rs Updates PermissionStatus conversion to handle new permission states
libs/config/deno_json/permissions.rs Adds AllowDenyIgnorePermissionConfig struct and deserialization support
libs/config/deno_json/mod.rs Exports new AllowDenyIgnore config types
ext/os/lib.rs Implements ignore behavior in env operations (get/set/delete/toObject), refactors NODE_ENV_VAR_ALLOWLIST to const array
cli/schemas/config-file.v1.json Defines JSON schema for ignore permission config
cli/args/mod.rs Integrates ignore_env into permissions options, renames no_op to identity for clarity
cli/args/flags.rs Adds ignore_env flag parsing, serialization, and test coverage

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"deny": { "$ref": "#/$defs/permissionConfigValue" },
"ignore": { "$ref": "#/$defs/permissionConfigValue" }
}
},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be used in "env" before merging.

@bartlomieju
Copy link
Member

Feels like it closes #29471 too

@dsherret dsherret marked this pull request as ready for review November 13, 2025 19:06
@dsherret
Copy link
Member Author

Feels like it closes #29471 too

Not sure about that because we might want to expand the number of allowed env variables by default.

Copy link
Member

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple comments, let's get some more eyes on this one too.

@nathanwhit
Copy link
Member

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 17, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Nov 17, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds an "ignore" permission path for environment variables: schema/types and config parsing to accept an ignore list, CLI flag plumbing for --ignore-env, permission model updates introducing PermissionState::Ignored and FlagIgnored descriptors with updated ordering, permission option propagation, and runtime env op changes so ignored env vars return undefined and are omitted from enumerations.

Changes

Cohort / File(s) Summary
Config schema & parsing
cli/schemas/config-file.v1.json, libs/config/deno_json/permissions.rs, libs/config/deno_json/mod.rs
Added schema defs and types for allow/deny/ignore (AllowDenyIgnorePermissionConfig, value variant, deserializer). Updated PermissionsObject.env to use the ignore-aware type and re-exported new types. Tests adjusted to expect ignore: None where appropriate.
CLI flag handling
cli/args/flags.rs, cli/args/mod.rs
Added ignore_env: Option<Vec<String>> to flags and to PermissionsOptions. Parsed and propagated --ignore-env and config-provided ignore lists into permission option construction and arg rendering.
Permission model & ordering
runtime/permissions/lib.rs
Introduced PermissionState::Ignored, FlagIgnored descriptor, precedence-based deny descriptor (order: u8), ordering updates (kind_precedence), has_flag_ignored/flag_ignored_global fields, new_unary_with_ignore constructor, ignore_env in PermissionsOptions, and derived ordering for NetDescriptor. Updated many internal APIs and tests to wire ignore semantics.
Permission state conversion
runtime/ops/permissions.rs
Updated From<PermissionState> to map Ignored (and denied variants) to external "denied" string while preserving partial/granted/prompt mappings.
Environment ops (get/set/delete/list)
ext/os/lib.rs
Added check_env_with_maybe_exit() that maps Ignored -> ControlFlow::Break to short-circuit operations. op_get_env, op_set_env, and op_delete_env now early-return when Ignored; op_env omits Ignored vars from listings and treats Ignored as non-granted for grant_all logic.
CLI/config tests & specs
tests/specs/permission/ignore_env/config/*, tests/specs/permission/ignore_env/flags/*
Added test suites and fixtures: config-based ignore (deno.json with permissions.default.env.ignore) and flag-based ignore (--ignore-env, --ignore-env=VAR) plus scripts asserting ignored vars return undefined and are excluded from toObject()/enumeration.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as CLI / Flags
    participant Config as Config Parser
    participant PermOpts as PermissionsOptions
    participant PermLib as Permission Lib
    participant EnvOp as Env Operation

    User->>CLI: deno run --ignore-env=VAR app.ts
    CLI->>Config: parse flags + config
    Config->>PermOpts: set ignore_env: ["VAR"]
    PermOpts->>PermLib: construct permissions (new_unary_with_ignore)
    PermLib->>PermLib: create FlagIgnored descriptor for "VAR"

    User->>EnvOp: Deno.env.get("VAR")
    EnvOp->>PermLib: check_env("VAR")
    PermLib-->>EnvOp: PermissionState::Ignored
    EnvOp->>User: return undefined

    User->>EnvOp: Deno.env.toObject()
    EnvOp->>PermLib: check env for each key
    PermLib-->>EnvOp: Ignored for "VAR", Granted for others
    EnvOp->>User: return object omitting Ignored keys
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • Areas needing focused review:
    • runtime/permissions/lib.rs: new Ignored state, descriptor ordering changes, many API/behavioral updates and derived trait additions.
    • ext/os/lib.rs: ControlFlow short-circuiting integration across env ops and correct early-return semantics.
    • CLI/config plumbing: consistent parsing/propagation of ignore lists from flags and config, and arg rendering behavior for empty vs present ignore lists.
    • Tests/specs: ensure coverage for empty lists, wildcard patterns, combinations with allow/deny, and both flag- and config-driven cases.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add --ignore-env=...' directly and concisely describes the main feature being added - a new command-line option to ignore environment variables.
Description check ✅ Passed The description clearly explains the new --ignore-env feature with concrete examples showing before/after behavior and references the closed issues.
Linked Issues check ✅ Passed The PR implements the core requirements from both issues: #30891 requests an 'ignore' mechanism for env vars returning undefined instead of erroring, and #31071 requests granular enumeration control. The changes add ignore_env support throughout the permission system and env operations.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the ignore-env feature: CLI flag parsing, permission config structures, permission state handling, environment operation logic, schema updates, and test cases. No unrelated modifications present.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cli/args/flags.rs (2)

4204-4249: Help text omission: add --ignore-env to Permission options.

The after_help block documents allow/deny env, but not ignore. Add it so users can discover this flag.

Apply this diff:

@@
       <g>--deny-env[=<<VARIABLE_NAME>...]</>      Deny access to environment variables. Optionally specify inacessible environment variables.
                                              <p(245)>--deny-env  |  --deny-env="PORT,HOME,PATH"</>
+      <g>--ignore-env[=<<VARIABLE_NAME>...]</>    Ignore reads of environment variables. Optionally specify variable names; when none are provided, all env reads return 'undefined' and env enumeration is empty.
+                                             <p(245)>--ignore-env  |  --ignore-env="PORT,HOME,PATH"</>

1261-1279: has_permission_in_argv() misses --ignore-env.

This causes false negatives when only --ignore-env is supplied. Include it (and consider --allow/--deny-import, but that’s optional here).

Apply this diff:

@@
   pub fn has_permission_in_argv(&self) -> bool {
     self.argv.iter().any(|arg| {
       arg == "--allow-all"
         || arg.starts_with("--allow-env")
         || arg.starts_with("--deny-env")
+        || arg.starts_with("--ignore-env")
         || arg.starts_with("--allow-ffi")
         || arg.starts_with("--deny-ffi")
         || arg.starts_with("--allow-net")
         || arg.starts_with("--deny-net")
         || arg.starts_with("--allow-read")
         || arg.starts_with("--deny-read")
         || arg.starts_with("--allow-run")
         || arg.starts_with("--deny-run")
         || arg.starts_with("--allow-sys")
         || arg.starts_with("--deny-sys")
         || arg.starts_with("--allow-write")
         || arg.starts_with("--deny-write")
     })
   }

Also extend the test to cover this case:

@@ fn has_permission_in_argv() {
-    let r = flags_from_vec(svec!["deno", "run", "x.ts"]);
+    let r = flags_from_vec(svec!["deno", "run", "x.ts"]);
     assert_eq!(r.unwrap().has_permission_in_argv(), false);
+
+    let r = flags_from_vec(svec!["deno", "run", "x.ts", "--ignore-env"]);
+    assert_eq!(r.unwrap().has_permission_in_argv(), true);
♻️ Duplicate comments (2)
cli/args/mod.rs (1)

1511-1715: ignore_env mapping reuses handle_deny semantics appropriately

Wiring ignore_env through handle_deny gives it the same precedence rules as the other list-style permissions (flags override config; All → empty vec sentinel; Some → normalized list), which matches how ignore lists are conceptually used here. The only minor nit is that handle_deny is now a generic “list from flags/config” helper rather than strictly deny-specific; if you ever touch this again, a more neutral name (or a brief comment noting it’s shared with ignore) could make the intent clearer.

runtime/permissions/lib.rs (1)

721-811: Ordering and precedence for FlagDenied/PromptDenied/FlagIgnored are correct but precedence u8 remains opaque

The refactor of UnaryPermissionDesc ordering to carry an explicit precedence for the deny variants and the addition of FlagIgnored integrates cleanly:

  • AllowOrDenyDescRef::Deny(_, precedence) plus kind_precedence() ensure a stable sort where FlagDenied < PromptDenied < FlagIgnored < Granted for equal descriptors.
  • UnaryPermissionDescriptors tracks has_flag_ignored and the combined has_any_denied_or_ignored() so that higher‑level logic can cheaply reason about any non‑granted, non‑prompt state.

Functionally this is solid and well covered by the comparison tests at the end of the file. The only readability nit is that raw u8 precedence values (0/1/2) are not self‑describing; an internal enum for the precedence level would make this easier to maintain, which matches the earlier reviewer suggestion.

Also applies to: 813-889

🧹 Nitpick comments (10)
cli/args/flags.rs (2)

9228-9247: Consider adding validator tests for --ignore-env.

Parity with allow/deny validators would help catch invalid keys (=, \0) for ignore too.

You can add:

@@
   fn ignore_env_ignorelist() {
@@
   }
+
+  #[test]
+  fn ignore_env_ignorelist_validator() {
+    let r = flags_from_vec(svec!["deno", "run", "--ignore-env=HOME", "script.ts"]);
+    assert!(r.is_ok());
+    let r = flags_from_vec(svec!["deno", "run", "--ignore-env=H=ME", "script.ts"]);
+    assert!(r.is_err());
+    let r = flags_from_vec(svec!["deno", "run", "--ignore-env=H\0ME", "script.ts"]);
+    assert!(r.is_err());
+  }

9292-9315: Test name typo.

The test uses --ignore-env but is named deny_env_ignorelist_multiple. Rename for clarity.

Apply this diff:

-  fn deny_env_ignorelist_multiple() {
+  fn ignore_env_ignorelist_multiple() {
libs/config/deno_json/permissions.rs (2)

128-165: Consider de-duplicating deserialize_allow_deny_ignore and deserialize_allow_deny

The bool and list branches here are effectively identical to deserialize_allow_deny, only differing in the target struct type and absence/presence of ignore. A small shared helper (parameterized over the target struct) would reduce the chance of these diverging on future changes.


308-312: Updated serde tests for env stay aligned with new type

The expectations for env now using AllowDenyIgnorePermissionConfig { allow: Some(...), deny: None, ignore: None } keep the tests validating backwards-compatible behavior for env: true and env: ["test"]. You might optionally add a case that exercises env: { "ignore": true } (or an ignore list) at this level to pin the new semantics in unit tests, but the spec tests will already cover behavior end-to-end.

Also applies to: 358-362

tests/specs/permission/ignore_env/flags/main.ts (1)

1-7: Flags test script accurately asserts ignore-env behavior

The script correctly verifies that VAR1 reads and enumeration are suppressed when ignored while still allowing VAR2. For slightly better diagnostics you could throw new Error("FAILED") instead of a bare string, but that’s strictly optional in this test context.

tests/specs/permission/ignore_env/config/main.ts (1)

1-7: Test script correctly validates ignore-env behavior

Logging Deno.env.get("VAR1"), Deno.env.get("VAR2"), and asserting "VAR1" in object fails covers the core semantics of ignored vars disappearing from toObject(). If you want slightly clearer stack traces, you could throw an Error instead of a string:

-if ("VAR1" in object) {
-  throw "FAILED";
-}
+if ("VAR1" in object) {
+  throw new Error("FAILED");
+}
tests/specs/permission/ignore_env/flags/__test__.jsonc (1)

1-26: Flag tests give good coverage of ignore/allow/deny env combinations

The four cases (--ignore-env, --ignore-env=VAR1, --ignore-env=VAR1 --allow-env, and --deny-env --ignore-env=VAR1) nicely exercise the intended interactions and should catch regressions around ignored vs denied variables and enumeration behavior.

If you want the fixtures to be a bit more uniform, you could align the error output formatting (spacing after error: and trailing newlines) across some and deny_env to avoid subtle snapshot differences, but that’s purely cosmetic.

ext/os/lib.rs (2)

7-7: Env permission helper and Ignored handling look consistent; confirm silent no-op semantics for set/delete

The new check_env_with_maybe_exit helper cleanly centralizes env checks and the mapping of PermissionState::Ignored to ControlFlow::Break(()), and its integration into op_get_env, op_set_env, and op_delete_env is coherent: denied states still surface as OsError::Permission, while ignored states short‑circuit the ops.

One behavioral nuance worth double‑checking: for ignored env vars, op_set_env and op_delete_env now become silent no‑ops (returning Ok(()) without touching the process env), and op_get_env returns Ok(None). That means JS callers will see Deno.env.set/delete succeed and Deno.env.get/toObject() behave as if the variable doesn’t exist. If the intent was to treat ignore as “read returns undefined” only, rather than also suppressing writes/deletes, you may want to reconsider whether writes should still error out for ignored vars.

Also applies to: 151-177, 179-192, 269-281, 283-300


206-218: Env enumeration behavior matches ignore semantics; consider aligning /proc/self/environ handling

The op_env changes correctly make check_env_all() treat GrantedPartial, DeniedPartial, and Ignored as “no global grant” while still proceeding with enumeration and filtering Ignored/Denied entries out of the returned map, which aligns with the --ignore-env / --allow-env + --deny-env goals.

Note that PermissionsContainer::check_env_all() is still used directly in check_special_file for /proc/self/fd/.../environ, where PermissionState::Ignored and partial denials will still bubble up as hard errors. If you want /proc/self/environ reads to behave consistently with Deno.env.toObject() under ignore/deny configurations, it may be worth special‑casing Ignored/partial states there as well.

Also applies to: 223-236, 4205-4227

runtime/permissions/lib.rs (1)

6507-6563: test_env_ignore covers key patterns; consider adding a global-ignore case

The new test_env_ignore nicely validates:

  • Simple allow/deny/ignore patterns (ALLOWED_*, DENIED_*, IGNORED_*), and
  • Overlapping patterns where ignore and allow both refine a broader deny (PREFIX_ALLOWED*, PREFIX*, PREFIX_IGNORED*),

with the expected Granted / Denied / Ignored outcomes.

One additional edge case you might want to cover here (even if CLI specs already test it) is the “global ignore” configuration where ignore_env is an explicit empty list (e.g., Permissions::new_unary_with_ignore(None, None, Some(Vec::new()), false)) to assert that all env queries return PermissionState::Ignored and that check_all() behaves as expected in that mode.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0cfbf5 and 5666ad5.

📒 Files selected for processing (13)
  • cli/args/flags.rs (8 hunks)
  • cli/args/mod.rs (5 hunks)
  • cli/schemas/config-file.v1.json (2 hunks)
  • ext/os/lib.rs (7 hunks)
  • libs/config/deno_json/mod.rs (1 hunks)
  • libs/config/deno_json/permissions.rs (5 hunks)
  • runtime/ops/permissions.rs (1 hunks)
  • runtime/permissions/lib.rs (25 hunks)
  • tests/specs/permission/ignore_env/config/__test__.jsonc (1 hunks)
  • tests/specs/permission/ignore_env/config/deno.json (1 hunks)
  • tests/specs/permission/ignore_env/config/main.ts (1 hunks)
  • tests/specs/permission/ignore_env/flags/__test__.jsonc (1 hunks)
  • tests/specs/permission/ignore_env/flags/main.ts (1 hunks)
🔇 Additional comments (18)
cli/args/flags.rs (6)

817-817: New permission flag field looks good.

Serde + Default semantics are correct for a new optional field; no compatibility concerns.


840-841: Correctly included in has_permission().

Good to account for ignore_env here.


979-988: to_permission_args: correct emission of --ignore-env.

Handles both empty (all ignored) and specific values. LGTM.


4179-4202: Helper for env-based deny/ignore flags is sound.

Consistent validation and Windows uppercasing match existing allow-env behavior.


4407-4408: Argument wiring: --ignore-env correctly added.

Reuses the shared validator; matches --deny-env. LGTM.


6639-6642: Parser plumbing for ignore-env is correct.

Values flow into flags.permissions.ignore_env and are logged for debug.

libs/config/deno_json/permissions.rs (2)

80-92: New AllowDenyIgnorePermissionConfig struct is consistent with existing pattern

The shape and is_none semantics line up with AllowDenyPermissionConfig while correctly treating any non-None ignore as “configured.” This keeps PermissionsObject::is_empty behavior coherent when env ignore is used.


191-192: env wiring to AllowDenyIgnorePermissionConfig looks correct

Switching env to AllowDenyIgnorePermissionConfig with deserialize_allow_deny_ignore preserves existing bool/array semantics while enabling env: { "ignore": ... } in configs. PermissionsObject::is_empty will continue to work via the new is_none implementation.

tests/specs/permission/ignore_env/config/__test__.jsonc (1)

1-8: Config-based ignore-env test spec matches intended semantics

The env setup, args (run --quiet -P main.ts), and expected output (undefined reads plus an empty [Object: null prototype] {}) correctly validate that default env.ignore: true causes all env probes and enumeration to be ignored.

tests/specs/permission/ignore_env/config/deno.json (1)

1-9: permissions.default.env.ignore: true is a clear config for “ignore all env vars”

This minimal config cleanly expresses “acknowledge env access but ignore all env variables,” which is exactly what the new ignore semantics are targeting.

libs/config/deno_json/mod.rs (1)

43-44: Public re-exports of ignore-aware permission types are appropriate

Re-exporting AllowDenyIgnorePermissionConfig and AllowDenyIgnorePermissionConfigValue alongside the existing Allow/Deny types cleanly surfaces the new env-ignore configuration model to callers without disrupting existing APIs.

runtime/ops/permissions.rs (1)

35-46: Mapping PermissionState::Ignored to "denied" is consistent with permission semantics

Treating Ignored alongside Denied/DeniedPartial as "denied" keeps the JS-facing PermissionStatus simple and correctly reflects that access is not granted; keeping partial tied only to GrantedPartial preserves existing behavior.

cli/schemas/config-file.v1.json (1)

19-35: Env permission schema cleanly supports allow/deny/ignore without allOf conflicts

Defining allowDenyIgnorePermissionConfig explicitly and pointing permissionSet.env at allowDenyIgnorePermissionConfigValue lets env permissions use boolean/array or an object with allow/deny/ignore, while keeping additionalProperties: false safe and consistent. This matches the new ignore semantics without breaking existing config shapes.

Also applies to: 72-72

cli/args/mod.rs (1)

1719-2017: Tests correctly validate env allow/deny/ignore resolution into PermissionsOptions

The updated test imports AllowDenyIgnorePermissionConfig, sets env with allow/deny/ignore, and asserts that flags_to_permissions_options produces ignore_env: Some(["env-ignore"]) in the first scenario and ignore_env: None when permissions.all = Some(true) and no explicit ignore is configured. This provides solid coverage for the new env-ignore configuration path and its interaction with the global all setting.

runtime/permissions/lib.rs (4)

127-139: Ignored permission state is wired consistently into query/check behavior

Adding PermissionState::Ignored and threading it through UnaryPermission::query_desc, is_allow_all, and is_partial_flag_denied matches the intended “internal ignore” semantics:

  • is_allow_all() now correctly treats any ignored global or descriptor as disqualifying a permission from being “fully granted”, preventing audit‑skip short‑circuits when ignores are configured.
  • Exact matches on ignored descriptors (or a global ignore flag) yield PermissionState::Ignored, while non‑matching descriptors still pass through the existing Granted/Denied/Partial logic.
  • is_partial_flag_denied treating FlagIgnored alongside FlagDenied only for the “no descriptor” case gives the desired DeniedPartial semantics for check_all() while keeping per‑descriptor queries precise.

This looks coherent and backwards‑compatible for non‑env permissions.

Also applies to: 939-947, 989-1014, 1139-1154


891-899: UnaryPermission global ignore flags and child propagation look correct

The addition of flag_ignored_global to UnaryPermission and its usage in:

  • Default/Clone,
  • is_allow_all() (via !self.flag_ignored_global and !descriptors.has_any_denied_or_ignored()), and
  • create_child_permissions() (copying both the global flag and FlagIgnored descriptors into the child),

ensures:

  • Global ignores prevent “fully granted” fast‑paths.
  • Ignore state is faithfully inherited by worker/child permissions, avoiding accidental privilege escalation in workers.

This matches how global denies are handled and keeps ignore semantics consistent across parent/child permissions.

Also applies to: 901-912, 914-925, 1199-1248


3372-3392: ignore_env plumbing via new_unary_with_ignore is generic and appears correct

PermissionsOptions gaining an ignore_env field and the introduction of Permissions::new_unary_with_ignore give a clean, generic way to construct permissions from allow/deny/ignore lists:

  • Capacity preallocation accounts for all three lists, and global_from_option properly interprets an explicit empty list as a “global” flag for each of allow/deny/ignore.
  • new_unary() now just delegates to new_unary_with_ignore with ignore_list = None, so existing callers stay unchanged.
  • Permissions::from_options wires env up via new_unary_with_ignore, while other permissions still use new_unary, keeping ignore semantics constrained to env as intended.
  • Permissions::none(...) still uses new_unary for env, so ignore behavior is only active when explicitly configured via flags/config.

Overall this is a solid extension point; if you later decide to support ignore semantics for other permission types, this structure will generalize easily.

Also applies to: 3411-3451, 3567-3578, 3640-3650


1780-1781: NetDescriptor ordering and comparisons align with new precedence logic

Deriving PartialOrd/Ord for NetDescriptor and implementing AllowDescriptor::cmp_allow/cmp_deny and DenyDescriptor::cmp_deny with host/port‑aware ordering looks consistent:

  • cmp_allow treats more specific descriptors (with ports) as “less than” their host‑only counterparts, which fits the “more specific first” rule used throughout the permission descriptor ordering.
  • cmp_deny layering on top of cmp_allow with Equal => Greater preserves the intended “deny wins over allow” behavior when descriptors are identical.
  • The NetDescriptor tests (test_net_ordering_with_ports, test_cmp_net_descriptors, etc.) exercise both allow/deny interactions and the new ordering rules, which is reassuring given how central these comparisons are to permission resolution.

I don’t see any ordering inconsistencies here.

Also applies to: 1857-1884, 1922-2052

Copy link
Member

@nathanwhit nathanwhit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just a couple small comments

PermissionState::DeniedPartial | PermissionState::Denied => "denied",
PermissionState::Ignored
| PermissionState::DeniedPartial
| PermissionState::Denied => "denied",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a new "ignored" permission status or is denied fine?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to change the public runtime api, but probably.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
runtime/permissions/lib.rs (1)

723-812: UnaryPermissionDesc ordering with FlagIgnored looks sound but numeric precedence remains opaque

The AllowOrDenyDescRef::Deny(_, u8) plus kind_precedence/local precedence cleanly extend the existing ordering to a third deny‑like kind, and the logic in Ord::cmp still obeys the expected invariants (deny vs allow, more specific before less specific, tests at the bottom cover the non‑Ignored cases).

One minor readability nit: the magic 0/1/2 precedence values for FlagDenied, PromptDenied, and FlagIgnored are still non‑self‑documenting. A small internal enum or named const values for these precedence levels would make this easier to maintain, especially now that there are three states instead of two.

🧹 Nitpick comments (2)
cli/args/mod.rs (1)

1549-1715: Deny/ignore helper and ignore_env wiring look consistent and correct

The new handle_deny_or_ignore helper correctly mirrors the existing precedence semantics: CLI vectors override config, PermissionConfigValue::All is encoded as Some(vec![]), and None keeps the field unset. Reusing it for all deny_* fields plus ignore_env keeps the behavior uniform and makes sentinel "all" semantics for --ignore-... and config "ignore": "all" align with existing deny handling.

The interaction with flags.allow_all (dropping config while still honoring explicit CLI deny/ignore flags) also seems intentional and reasonable for env, net, fs, etc. Overall this centralization should be low‑risk and improves maintainability.

If you want to further clarify behavior later, a small comment on handle_deny_or_ignore noting that Some(vec![]) means “all denied/ignored” could help future readers, but not required for this PR.

runtime/permissions/lib.rs (1)

6509-6565: test_env_ignore covers key allow/deny/ignore precedence cases; consider adding a global‑ignore check

The new test validates both:

  • Simple allow vs deny vs ignore (ALLOWED_*, DENIED_*, IGNORED_*).
  • Overlapping patterns where a more specific ignore (PREFIX_IGNORED*) and allow (PREFIX_ALLOWED*) must outrank a broader deny (PREFIX*).

This gives good coverage of the intended precedence rules. As a small enhancement, you might add a case for the “global ignore” form (ignore_env: Some(vec![])) to assert that queries report PermissionState::Ignored both for specific vars and for query(None), mirroring the CLI --ignore-env behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5666ad5 and 630caf7.

📒 Files selected for processing (3)
  • cli/args/mod.rs (12 hunks)
  • cli/schemas/config-file.v1.json (2 hunks)
  • runtime/permissions/lib.rs (25 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • cli/schemas/config-file.v1.json
🧰 Additional context used
🧬 Code graph analysis (1)
runtime/permissions/lib.rs (3)
cli/tsc/dts/lib.deno.ns.d.ts (1)
  • PermissionState (4062-4062)
runtime/permissions/prompter.rs (4)
  • prompt (96-103)
  • prompt (110-119)
  • prompt (321-525)
  • prompt (538-551)
runtime/ops/permissions.rs (1)
  • from (36-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: test debug linux-x86_64
  • GitHub Check: test debug linux-aarch64
  • GitHub Check: test release windows-x86_64
  • GitHub Check: test debug windows-x86_64
  • GitHub Check: test debug macos-x86_64
  • GitHub Check: test release macos-aarch64
  • GitHub Check: test debug macos-aarch64
  • GitHub Check: test release macos-x86_64
  • GitHub Check: lint debug windows-x86_64
  • GitHub Check: lint debug linux-x86_64
  • GitHub Check: lint debug macos-x86_64
  • GitHub Check: build libs
🔇 Additional comments (8)
cli/args/mod.rs (1)

1719-2015: Tests cover env ignore config and new PermissionsOptions field

The test updates (using AllowDenyIgnorePermissionConfig for env and asserting ignore_env in PermissionsOptions) solidly exercise the new wiring from config → flags_to_permissions_options. The first case validates non‑empty env.ignore propagation; the second checks that all: Some(true) does not implicitly enable ignore and that the default ignore_env is None.

This gives good regression coverage around the new field without overcomplicating the fixture.

runtime/permissions/lib.rs (7)

129-141: New PermissionState::Ignored is consistent with existing status mapping

Introducing Ignored as a distinct internal state while still mapping it to "denied" for the public PermissionStatus (via runtime/ops/permissions.rs) keeps the JS surface stable and gives env ops a clean hook to “soft‑deny” reads. The enum layout and discriminant choice look fine; I don’t see any ordering or matching pitfalls here.


815-921: Tracking has_flag_ignored / flag_ignored_global integrates cleanly into is_allow_all

Adding has_flag_ignored to UnaryPermissionDescriptors and flag_ignored_global to UnaryPermission, and folding them into has_any_denied_or_ignored() and is_allow_all(), correctly ensures that any explicit ignore (global or per‑descriptor) prevents a permission from being treated as “fully granted”. That matches the semantics that “ignore” is still a restriction and should block fast‑path/all‑granted optimizations.


991-1050: Env ignore semantics in query_desc and child permissions are logically consistent

  • query_allowed_desc_for_exact_match now returns PermissionState::Ignored when a query matches a FlagIgnored descriptor, and query_desc short‑circuits to Ignored when flag_ignored_global is set. This gives a clear, first‑class “ignored” result that callers (env ops, Deno.permissions.*) can distinguish from Denied and Prompt.
  • is_partial_flag_denied treating FlagIgnored the same as FlagDenied for partial/“*_Partial” states is reasonable: ignores are still effectively denials when deciding if a permission is partially constrained.
  • create_child_permissions propagates flag_ignored_global and carries over all FlagIgnored descriptors, preserving ignore semantics for workers and preventing privilege escalation (since the child allow‑list is still checked against the parent’s permissions before cloning).

Given the new unit tests and env‑wildcard tests, this path looks correct.

Also applies to: 1141-1156, 1201-1253


1782-2057: NetDescriptor rework (Query/Allow/Deny + ordering) matches intended host/port semantics

Making NetDescriptor itself the QueryDesc, AllowDesc, and DenyDesc simplifies the model and the new comparison logic behaves as expected:

  • cmp_allow prefers more specific descriptors (with port) before less specific ones (no port), and then orders by Host then port, which aligns with the new tests in test_net_ordering_with_ports and test_cmp_net_descriptors.
  • AllowDescriptor::cmp_deny biases towards deny entries when descriptors are equal, while DenyDescriptor::cmp_deny uses the same structural ordering as allows, which fits the general pattern used for other permission types and keeps UnaryPermissionDesc’s sort stable.
  • Parsing in parse_inner still covers vsock, bracketed IPv6, IPv4/FQDN with optional ports, and rejects URL‑like inputs, with extensive tests for both parse_for_query and parse_for_list.

I don’t see any net‑permission regressions in this implementation.


3374-3394: PermissionsOptions.ignore_env is a minimal, backwards‑compatible extension

Adding ignore_env: Option<Vec<String>> to PermissionsOptions with existing serde derives provides the new configuration surface without impacting other permissions. No issues here; it’s structurally consistent with allow_env/deny_env.


3413-3452: Permissions::new_unary_with_ignore and wrapper new_unary clean up construction

The new helper cleanly centralizes allow/deny/ignore list handling:

  • Capacity preallocation accounts for all three lists.
  • global_from_option for each list keeps the existing “empty vector means global flag” convention and extends it to ignore.
  • The struct update ..Default::default() only fills prompt_denied_global (and future fields), avoiding duplication.

Using new_unary as a thin wrapper for non‑ignore permissions keeps call sites simple while allowing env to opt into ignore semantics. This looks well‑factored.


3569-3580: Env permissions correctly wired to ignore_env in Permissions::from_options

The env construction now passes three independently parsed lists into new_unary_with_ignore, so CLI/config combinations like:

  • --allow-env=...
  • --deny-env=...
  • --ignore-env=... / --ignore-env (empty list for global ignore)

all map directly into the internal representation. Parsing paths are symmetric with the existing allow/deny env handling, and other permissions still go through new_unary, so this change is nicely localized.

@dsherret dsherret enabled auto-merge (squash) November 19, 2025 14:43
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
runtime/permissions/lib.rs (3)

129-141: PermissionState::Ignored is wired consistently but consider documenting intent

The new Ignored variant integrates cleanly with the rest of this module and, via PermissionStatus mapping, will surface as "denied" to JS callers, which matches the “return undefined instead of throwing” semantics for env. One small follow-up: the /// Quadri-state comment above the enum is now inaccurate and doesn’t explain how Ignored is intended to differ from Denied from the engine’s perspective; a brief doc tweak here would help future readers.


822-898: Ignored tracking in UnaryPermission and query logic correctly mirror deny semantics

The additions of:

  • has_flag_ignored and has_any_denied_or_ignored in UnaryPermissionDescriptors,
  • flag_ignored_global in UnaryPermission,
  • the new branches in is_allow_all, query_desc, query_allowed_desc_for_exact_match, is_partial_flag_denied, and
  • propagation of flag_ignored_global and FlagIgnored descriptors in create_child_permissions

are internally consistent and match the intended behavior:

  • is_allow_all now returns false whenever any explicit deny, prompt-deny, or ignore exists (global or descriptor-based), preventing the audit macro from bypassing ignore logic.
  • query_desc returns Ignored either when an exact FlagIgnored descriptor matches or when flag_ignored_global is set, giving a clear signal for env ops to treat reads as “acknowledged but value hidden.”
  • is_partial_flag_denied now treats ignored descriptors like denied ones for the global (None) case, so Deno.permissions.query({ name: "env" }) can correctly report partial states when there are ignored patterns, while per-var env queries still rely on the specific match ordering and are unaffected because EnvQueryDescriptor::overlaps_deny is false.
  • create_child_permissions copies both global and per-descriptor ignores from parent to child, and escalation checks run through the same check_in_permission path, so children can’t “un-ignore” a parent’s env patterns.

Given the targeted tests (test_env_wildcards, test_env_ignore) and existing path/sys/net/ffi/import tests, this design looks correct and robust.

Also applies to: 900-955, 998-1023, 1048-1163, 1208-1258


6516-6572: test_env_ignore gives good coverage of ignore vs allow vs deny precedence

The new test exercises both disjoint and overlapping prefix patterns for env:

  • First block validates that ALLOWED_*, IGNORED_*, and DENIED_* lead to Granted, Ignored, and Denied respectively.
  • Second block checks nested patterns where a broad deny (PREFIX*) coexists with a more specific allow (PREFIX_ALLOWED*) and a more specific ignore (PREFIX_IGNORED*), and the outcomes (Denied, Ignored, Granted) are as desired.

This nicely anchors the intended ordering behavior for FlagIgnored without impacting existing env wildcard tests. If you later support global-only ignore without any allow/deny, consider adding a small case for ignore_env: Some(vec![]) to lock in the “everything ignored” behavior as well, but it’s not strictly necessary for this PR.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 630caf7 and 2460399.

📒 Files selected for processing (1)
  • runtime/permissions/lib.rs (25 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
runtime/permissions/lib.rs (3)
cli/tsc/dts/lib.deno.ns.d.ts (1)
  • PermissionState (4062-4062)
runtime/permissions/prompter.rs (4)
  • prompt (96-103)
  • prompt (110-119)
  • prompt (321-525)
  • prompt (538-551)
runtime/ops/permissions.rs (1)
  • from (36-47)
🔇 Additional comments (3)
runtime/permissions/lib.rs (3)

723-820: Ordering changes for UnaryPermissionDesc (including FlagIgnored) look sound

Moving AllowOrDenyDescRef::Deny to a struct with an explicit order and extending UnaryPermissionDesc with FlagIgnored plus the updated Ord implementation provides a clear, deterministic total ordering:

  • For equal descriptors, precedence is FlagDenied (0) < PromptDenied (1) < FlagIgnored (2) < Granted (3).
  • For different descriptors, the existing allow/deny cmp_* semantics remain intact, so “more specific” descriptors are still favored regardless of type.

Given the extensive comparison tests at the bottom of the file for read/write/net/env/sys/ffi/import descriptors (which still exercise the same ordering logic), this looks correct and backward-compatible, with FlagIgnored naturally treated as the weakest deny flavor.


1789-1791: Deriving PartialOrd/Ord on NetDescriptor is reasonable but keep it aligned with cmp_allow

Adding PartialOrd/Ord for NetDescriptor matches its existing Host/Option<u32> ordering and should be safe, as permission ordering still goes through cmp_allow/cmp_deny. Just be mindful that any new code that relies on NetDescriptor’s derived Ord should have expectations consistent with cmp_allow’s notion of specificity (ports vs host-only), to avoid subtle mismatches later.


3381-3401: ignore_env plumbing and new_unary_with_ignore construction match the desired semantics

The new pieces work together as expected:

  • PermissionsOptions.ignore_env: Option<Vec<String>> is parsed alongside allow_env/deny_env.
  • Permissions::new_unary_with_ignore constructs a UnaryPermission with:
    • granted_global/flag_denied_global/flag_ignored_global driven by empty-vector-as-global via global_from_option,
    • descriptors containing any explicit allow/deny/ignore entries, and
    • correct capacity preallocation.
  • Permissions::new_unary is now just a thin wrapper for the old two-list behavior (ignore_list=None), so existing callers remain behaviorally identical.
  • In from_options, only env uses new_unary_with_ignore, feeding in parsed allow/deny/ignore env descriptors, so other permissions are untouched.

This gives the intended combinations:

  • --ignore-env alone ⇒ flag_ignored_global = true, no allows/denies ⇒ per-var queries return Ignored, and enumeration can be handled non-throwingly in ext/os.
  • --ignore-env=VAR1 --allow-env=VAR2VAR1 yields Ignored, VAR2 Granted, others follow the existing allow/deny rules.
  • --allow-env --deny-env=SOMETHING semantics are unchanged at the engine level, so the new “no error on enumeration” behavior can be implemented at the env-op layer by interpreting Denied/Ignored as “filter out.”

This wiring looks correct and matches the PR objectives.

Also applies to: 3419-3459, 3550-3587

@dsherret dsherret merged commit 82eba47 into denoland:main Nov 19, 2025
33 of 37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Spefic permission for enumerating environment keys. Permission configuration: Acknowledging a permission being read, but 'ignore' it.

3 participants